Pitfalls of Demonstration
I came across an article called "Pitfalls of Safe Rust", which I just couldn't pass by, because of such demonstrations.
First of all, I see the listed examples with overflow:
// 1
fn calculate_total(price: u32, quantity: u32) -> u32 {
price * quantity // Could overflow!
}
//2
fn main() {
let x: u8 = 2;
let y: u8 = 128;
let z = x * y;
}
Here firstly, u8 is an 8-bit integer that takes values from 0 to 255. Secondly, we multiply 2 by 128 and get 256. Of course, this will lead to overflow.
In the provided example, the values 2 and 128 are valid because the u8 type is allocated for them. What would we do otherwise, specifying a larger type for the output:
fn main() {
let x: u8 = 2;
let y: u8 = 128;
let z: u16 = x as u16 * y as u16; // all good bro no worries
}
Or even so:
fn no_worries(x: u8, y: u8) -> u16 {
(x * y).into()
}
fn main() {
let x: u8 = 2;
let y: u8 = 128;
no_worries(x, y);
}
But that's not all. Let's go back to the example with the calculate_total() function and expand it for clarity. In my case, the calculate_total() function example didn't provide clarity until I started expanding and experimenting:
fn main() {
calculate_total(8);
}
fn calculate_total(value: u8) -> u8 {
let multiplier: u8 = 200;
value * multiplier
}
Here emerges an interesting picture. The compiler wasn't able to warn me about the potential problem before runtime. As a result, a panic occurs:
thread 'main' panicked at src/main.rs:7:5:
attempt to multiply with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This reveals a significant gap in analysis capabilities. The compiler doesn't see that we're calling the function with the specific value 8 because it analyzes the function definition independently of how it's used.
However, Rust provides tools for working with such cases. For our case with the calculate_total() function, this would be suitable:
/// Checked integer multiplication. Computes `self * rhs`, returning
/// `None` if overflow occurred.
///
/// # Examples
///
/// Basic usage:
///
/// ```
#[doc = concat!("assert_eq!(5", stringify!($SelfT), ".checked_mul(1), Some(5));")]
#[doc = concat!("assert_eq!(", stringify!($SelfT), "::MAX.checked_mul(2), None);")]
/// ```
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_checked_int_methods", since = "1.47.0")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub const fn checked_mul(self, rhs: Self) -> Option<Self> {
let (a, b) = self.overflowing_mul(rhs);
if intrinsics::unlikely(b) { None } else { Some(a) }
}
How can we apply the solution? In this case, the checked_mul() function pattern is combined with our calculate_total() function. So, we take the problematic part:
fn main() {
calculate_total(8);
}
fn calculate_total(value: u8) -> u8 {
let multiplier: u8 = 200;
value * multiplier
}
And we treat it with the checked_mul():
fn main() {
match calculate_total(8) {
Ok(result) => println!("Result: {}", result),
Err(err) => println!("Error: {}", err),
}
}
fn calculate_total(value: u8) -> Result<u8, &'static str> {
let multiplier: u8 = 200;
value.checked_mul(multiplier).ok_or("Overflow occurred")
}
After which, instead of panic, we get the following output:
Error: Overflow occurred
My verdict is that there are no universal solutions, and therenever will be. It's practically impossible to cover all cases and handle everything.